﻿/************************************************************************

   Extended WPF Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license 

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. The commercial version of Extended WPF Toolkit also includes
   priority technical support, commercial updates, and many additional 
   useful WPF controls if you license Xceed Business Suite for WPF.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Xceed.Wpf.Toolkit
{
  public class MaskedTextBox : TextBox
  {
    #region Members

    /// <summary>
    /// Flags if the Text and Value properties are in the process of being sync'd
    /// </summary>
    private bool _isSyncingTextAndValueProperties;
    private bool _isInitialized;
    private bool _convertExceptionOccurred = false;

    #endregion //Members

    #region Properties

    protected MaskedTextProvider MaskProvider
    {
      get;
      set;
    }

    #region IncludePrompt

    public static readonly DependencyProperty IncludePromptProperty = DependencyProperty.Register( "IncludePrompt", typeof( bool ), typeof( MaskedTextBox ), new UIPropertyMetadata( false, OnIncludePromptPropertyChanged ) );
    public bool IncludePrompt
    {
      get
      {
        return ( bool )GetValue( IncludePromptProperty );
      }
      set
      {
        SetValue( IncludePromptProperty, value );
      }
    }

    private static void OnIncludePromptPropertyChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox maskedTextBox = o as MaskedTextBox;
      if( maskedTextBox != null )
        maskedTextBox.OnIncludePromptChanged( ( bool )e.OldValue, ( bool )e.NewValue );
    }

    protected virtual void OnIncludePromptChanged( bool oldValue, bool newValue )
    {
      ResolveMaskProvider( Mask );
    }

    #endregion //IncludePrompt

    #region IncludeLiterals

    public static readonly DependencyProperty IncludeLiteralsProperty = DependencyProperty.Register( "IncludeLiterals", typeof( bool ), typeof( MaskedTextBox ), new UIPropertyMetadata( true, OnIncludeLiteralsPropertyChanged ) );
    public bool IncludeLiterals
    {
      get
      {
        return ( bool )GetValue( IncludeLiteralsProperty );
      }
      set
      {
        SetValue( IncludeLiteralsProperty, value );
      }
    }

    private static void OnIncludeLiteralsPropertyChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox maskedTextBox = o as MaskedTextBox;
      if( maskedTextBox != null )
        maskedTextBox.OnIncludeLiteralsChanged( ( bool )e.OldValue, ( bool )e.NewValue );
    }

    protected virtual void OnIncludeLiteralsChanged( bool oldValue, bool newValue )
    {
      ResolveMaskProvider( Mask );
    }

    #endregion //IncludeLiterals

    #region Mask

    public static readonly DependencyProperty MaskProperty = DependencyProperty.Register( "Mask", typeof( string ), typeof( MaskedTextBox ), new UIPropertyMetadata( default( String ), OnMaskPropertyChanged ) );
    public string Mask
    {
      get
      {
        return ( string )GetValue( MaskProperty );
      }
      set
      {
        SetValue( MaskProperty, value );
      }
    }

    private static void OnMaskPropertyChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox maskedTextBox = o as MaskedTextBox;
      if( maskedTextBox != null )
        maskedTextBox.OnMaskChanged( ( string )e.OldValue, ( string )e.NewValue );
    }

    protected virtual void OnMaskChanged( string oldValue, string newValue )
    {
      ResolveMaskProvider( newValue );
      UpdateText( 0 );
    }

    #endregion //Mask

    #region PromptChar

    public static readonly DependencyProperty PromptCharProperty = DependencyProperty.Register( "PromptChar", typeof( char ), typeof( MaskedTextBox ), new UIPropertyMetadata( '_', OnPromptCharChanged ) );
    public char PromptChar
    {
      get
      {
        return ( char )GetValue( PromptCharProperty );
      }
      set
      {
        SetValue( PromptCharProperty, value );
      }
    }

    private static void OnPromptCharChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox maskedTextBox = o as MaskedTextBox;
      if( maskedTextBox != null )
        maskedTextBox.OnPromptCharChanged( ( char )e.OldValue, ( char )e.NewValue );
    }

    protected virtual void OnPromptCharChanged( char oldValue, char newValue )
    {
      ResolveMaskProvider( Mask );
    }

    #endregion //PromptChar

    #region SelectAllOnGotFocus

    public static readonly DependencyProperty SelectAllOnGotFocusProperty = DependencyProperty.Register( "SelectAllOnGotFocus", typeof( bool ), typeof( MaskedTextBox ), new PropertyMetadata( false ) );
    public bool SelectAllOnGotFocus
    {
      get
      {
        return ( bool )GetValue( SelectAllOnGotFocusProperty );
      }
      set
      {
        SetValue( SelectAllOnGotFocusProperty, value );
      }
    }

    #endregion //SelectAllOnGotFocus

    #region Text

    private static void OnTextChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox inputBase = o as MaskedTextBox;
      if( inputBase != null )
        inputBase.OnTextChanged( ( string )e.OldValue, ( string )e.NewValue );
    }

    protected virtual void OnTextChanged( string oldValue, string newValue )
    {
      if( _isInitialized )
        SyncTextAndValueProperties( MaskedTextBox.TextProperty, newValue );
    }

    #endregion //Text

    #region Value

    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( "Value", typeof( object ), typeof( MaskedTextBox ), new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnValueChanged ) );
    public object Value
    {
      get
      {
        return ( object )GetValue( ValueProperty );
      }
      set
      {
        SetValue( ValueProperty, value );
      }
    }

    private static void OnValueChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox maskedTextBox = o as MaskedTextBox;
      if( maskedTextBox != null )
        maskedTextBox.OnValueChanged( ( object )e.OldValue, ( object )e.NewValue );
    }

    protected virtual void OnValueChanged( object oldValue, object newValue )
    {
      if( _isInitialized )
        SyncTextAndValueProperties( MaskedTextBox.ValueProperty, newValue );

      RoutedPropertyChangedEventArgs<object> args = new RoutedPropertyChangedEventArgs<object>( oldValue, newValue );
      args.RoutedEvent = MaskedTextBox.ValueChangedEvent;
      RaiseEvent( args );
    }

    #endregion //Value

    #region ValueType

    public static readonly DependencyProperty ValueTypeProperty = DependencyProperty.Register( "ValueType", typeof( Type ), typeof( MaskedTextBox ), new UIPropertyMetadata( typeof( String ), OnValueTypeChanged ) );
    public Type ValueType
    {
      get
      {
        return ( Type )GetValue( ValueTypeProperty );
      }
      set
      {
        SetValue( ValueTypeProperty, value );
      }
    }

    private static void OnValueTypeChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      MaskedTextBox maskedTextBox = o as MaskedTextBox;
      if( maskedTextBox != null )
        maskedTextBox.OnValueTypeChanged( ( Type )e.OldValue, ( Type )e.NewValue );
    }

    protected virtual void OnValueTypeChanged( Type oldValue, Type newValue )
    {
      if( _isInitialized )
        SyncTextAndValueProperties( MaskedTextBox.TextProperty, Text );
    }

    #endregion //ValueType

    #endregion //Properties

    #region Constructors

    static MaskedTextBox()
    {
      TextProperty.OverrideMetadata( typeof( MaskedTextBox ), new FrameworkPropertyMetadata( OnTextChanged ) );
    }

    public MaskedTextBox()
    {
      CommandBindings.Add( new CommandBinding( ApplicationCommands.Paste, Paste ) ); //handle paste
      CommandBindings.Add( new CommandBinding( ApplicationCommands.Cut, null, CanCut ) ); //surpress cut
    }

    #endregion //Constructors

    #region Base Class Overrides

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      UpdateText( 0 );
    }

    protected override void OnInitialized( EventArgs e )
    {
      base.OnInitialized( e );

      if( !_isInitialized )
      {
        _isInitialized = true;
        SyncTextAndValueProperties( ValueProperty, Value );
      }
    }

    protected override void OnGotKeyboardFocus( KeyboardFocusChangedEventArgs e )
    {
      if( SelectAllOnGotFocus )
      {
        SelectAll();
      }

      base.OnGotKeyboardFocus( e );
    }

    protected override void OnPreviewKeyDown( KeyEventArgs e )
    {
      if( !e.Handled )
      {
        HandlePreviewKeyDown( e );
      }

      base.OnPreviewKeyDown( e );
    }

    protected override void OnPreviewTextInput( TextCompositionEventArgs e )
    {
      if( !e.Handled )
      {
        HandlePreviewTextInput( e );
      }

      base.OnPreviewTextInput( e );
    }

    #endregion //Base Class Overrides

    #region Events

    public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent( "ValueChanged", RoutingStrategy.Bubble, typeof( RoutedPropertyChangedEventHandler<object> ), typeof( MaskedTextBox ) );
    public event RoutedPropertyChangedEventHandler<object> ValueChanged
    {
      add
      {
        AddHandler( ValueChangedEvent, value );
      }
      remove
      {
        RemoveHandler( ValueChangedEvent, value );
      }
    }

    #endregion //Events

    #region Methods

    #region Private

    private void UpdateText()
    {
      UpdateText( SelectionStart );
    }

    private void UpdateText( int position )
    {
      MaskedTextProvider provider = MaskProvider;
      if( provider == null )
        throw new InvalidOperationException();

      Text = provider.ToDisplayString();
      SelectionLength = 0;
      SelectionStart = position;
    }

    private int GetNextCharacterPosition( int startPosition )
    {
      int position = MaskProvider.FindEditPositionFrom( startPosition, true );
      return position == -1 ? startPosition : position;
    }

    private void ResolveMaskProvider( string mask )
    {
      //do not create a mask provider if the Mask is empty, which can occur if the IncludePrompt and IncludeLiterals properties
      //are set prior to the Mask.
      if( String.IsNullOrEmpty( mask ) )
        return;

      MaskProvider = new MaskedTextProvider( mask )
      {
        IncludePrompt = this.IncludePrompt,
        IncludeLiterals = this.IncludeLiterals,
        PromptChar = this.PromptChar,
        ResetOnSpace = false //should make this a property
      };
    }

    private object ConvertTextToValue( string text )
    {
      object convertedValue = null;

      Type dataType = ValueType;

      string valueToConvert = MaskProvider.ToString().Trim();

      try
      {
        if( valueToConvert.GetType() == dataType || dataType.IsInstanceOfType( valueToConvert ) )
        {
          convertedValue = valueToConvert;
        }
#if !VS2008
        else if( String.IsNullOrWhiteSpace( valueToConvert ) )
        {
          convertedValue = Activator.CreateInstance( dataType );
        }
#else
        else if ( String.IsNullOrEmpty( valueToConvert ) )
        {
          convertedValue = Activator.CreateInstance( dataType );
        }
#endif
        else if( null == convertedValue && valueToConvert is IConvertible )
        {
          convertedValue = Convert.ChangeType( valueToConvert, dataType );
        }
      }
      catch
      {
        //if an excpetion occurs revert back to original value
        _convertExceptionOccurred = true;
        return Value;
      }

      return convertedValue;
    }

    private string ConvertValueToText( object value )
    {
      if( value == null )
        value = string.Empty;

      if( _convertExceptionOccurred )
      {
        value = Value;
        _convertExceptionOccurred = false;
      }

      //I have only seen this occur while in Blend, but we need it here so the Blend designer doesn't crash.
      if( MaskProvider == null )
        return value.ToString();

      MaskProvider.Set( value.ToString() );
      return MaskProvider.ToDisplayString();
    }

    private void SyncTextAndValueProperties( DependencyProperty p, object newValue )
    {
      //prevents recursive syncing properties
      if( _isSyncingTextAndValueProperties )
        return;

      _isSyncingTextAndValueProperties = true;

      //this only occures when the user typed in the value
      if( MaskedTextBox.TextProperty == p )
      {
        if( newValue != null )
          SetValue( MaskedTextBox.ValueProperty, ConvertTextToValue( newValue.ToString() ) );
      }

      SetValue( MaskedTextBox.TextProperty, ConvertValueToText( newValue ) );

      _isSyncingTextAndValueProperties = false;
    }

    private void HandlePreviewTextInput( TextCompositionEventArgs e )
    {
      if( !IsReadOnly )
      {
        this.InsertText( e.Text );
      }

      e.Handled = true;
    }

    private void HandlePreviewKeyDown( KeyEventArgs e )
    {
      if( e.Key == Key.Delete )
      {
        e.Handled = IsReadOnly
                 || HandleKeyDownDelete();
      }
      else if( e.Key == Key.Back )
      {
        e.Handled = IsReadOnly
                 || HandleKeyDownBack();
      }
      else if( e.Key == Key.Space )
      {
        if( !IsReadOnly )
        {
          InsertText( " " );
        }

        e.Handled = true;
      }
      else if( e.Key == Key.Return || e.Key == Key.Enter )
      {
        if( !IsReadOnly && AcceptsReturn )
        {
          this.InsertText( "\r" );
        }

        // We don't want the OnPreviewTextInput to be triggered for the Return/Enter key
        // when it is not accepted.
        e.Handled = true;
      }
      else if( e.Key == Key.Escape )
      {
        // We don't want the OnPreviewTextInput to be triggered at all for the Escape key.
        e.Handled = true;
      }
      else if( e.Key == Key.Tab )
      {
        if( AcceptsTab )
        {
          if( !IsReadOnly )
          {
            this.InsertText( "\t" );
          }

          e.Handled = true;
        }
      }
    }

    private bool HandleKeyDownDelete()
    {
      ModifierKeys modifiers = Keyboard.Modifiers;
      bool handled = true;

      if( modifiers == ModifierKeys.None )
      {
        if( !RemoveSelectedText() )
        {
          int position = SelectionStart;

          if( position < Text.Length )
          {
            RemoveText( position, 1 );
            UpdateText( position );
          }
        }
        else
        {
          UpdateText();
        }
      }
      else if( modifiers == ModifierKeys.Control )
      {
        if( !RemoveSelectedText() )
        {
          int position = SelectionStart;

          RemoveTextToEnd( position );
          UpdateText( position );
        }
        else
        {
          UpdateText();
        }
      }
      else if( modifiers == ModifierKeys.Shift )
      {
        if( RemoveSelectedText() )
        {
          UpdateText();
        }
        else
        {
          handled = false;
        }
      }
      else
      {
        handled = false;
      }

      return handled;
    }

    private bool HandleKeyDownBack()
    {
      ModifierKeys modifiers = Keyboard.Modifiers;
      bool handled = true;

      if( modifiers == ModifierKeys.None || modifiers == ModifierKeys.Shift )
      {
        if( !RemoveSelectedText() )
        {
          int position = SelectionStart;

          if( position > 0 )
          {
            int newPosition = position - 1;

            RemoveText( newPosition, 1 );
            UpdateText( newPosition );
          }
        }
        else
        {
          UpdateText();
        }
      }
      else if( modifiers == ModifierKeys.Control )
      {
        if( !RemoveSelectedText() )
        {
          RemoveTextFromStart( SelectionStart );
          UpdateText( 0 );
        }
        else
        {
          UpdateText();
        }
      }
      else
      {
        handled = false;
      }

      return handled;
    }

    private void InsertText( string text )
    {
      int position = SelectionStart;
      MaskedTextProvider provider = MaskProvider;

      bool textRemoved = this.RemoveSelectedText();

      position = GetNextCharacterPosition( position );

      if( !textRemoved && Keyboard.IsKeyToggled( Key.Insert ) )
      {
        if( provider.Replace( text, position ) )
        {
          position += text.Length;
        }
      }
      else
      {
        if( provider.InsertAt( text, position ) )
        {
          position += text.Length;
        }
      }

      position = GetNextCharacterPosition( position );

      this.UpdateText( position );
    }

    private void RemoveTextFromStart( int endPosition )
    {
      RemoveText( 0, endPosition );
    }

    private void RemoveTextToEnd( int startPosition )
    {
      RemoveText( startPosition, Text.Length - startPosition );
    }

    private void RemoveText( int position, int length )
    {
      if( length == 0 )
        return;

      MaskProvider.RemoveAt( position, position + length - 1 );
    }

    private bool RemoveSelectedText()
    {
      int length = SelectionLength;

      if( length == 0 )
        return false;

      int position = SelectionStart;

      return MaskProvider.RemoveAt( position, position + length - 1 );
    }

    #endregion //Private

    #endregion //Methods

    #region Commands

    private void Paste( object sender, RoutedEventArgs e )
    {
      if( IsReadOnly )
        return;

      object data = Clipboard.GetData( DataFormats.Text );
      if( data != null )
      {
        string text = data.ToString().Trim();
        if( text.Length > 0 )
        {
          int position = SelectionStart;

          MaskProvider.Set( text );

          UpdateText( position );
        }
      }
    }

    private void CanCut( object sender, CanExecuteRoutedEventArgs e )
    {
      e.CanExecute = false;
      e.Handled = true;
    }

    #endregion //Commands
  }
}
